Skip to content

FrameLab v1.1.0: Basler/backend support, audit (complexity + bug fixes + tests), dependency pinning#7

Draft
andrefecto wants to merge 73 commits into
mainfrom
dev-v1.1.0
Draft

FrameLab v1.1.0: Basler/backend support, audit (complexity + bug fixes + tests), dependency pinning#7
andrefecto wants to merge 73 commits into
mainfrom
dev-v1.1.0

Conversation

@andrefecto
Copy link
Copy Markdown
Owner

FrameLab v1.1.0 — dev-v1.1.0main

Large release: 53 commits, 71 files, +9,664 / −1,856. Two bodies of work:
(A) the v1.1.0 feature/performance/platform work, and (B) a full codebase
audit (complexity reduction + bug hardening + tests + supply-chain pinning).

Opening as draft — being smoke-tested on the customer's machine tonight before marking ready.


A. Features / performance / platform

  • Basler Ace 2 camera support (GenICam controls) via a new camera backend abstraction layer
    (camera/source.py, source_factory.py, basler_source.py) — OpenCV and Basler behind one interface.
  • Windows camera reliability: switched DirectShow → Media Foundation (MSMF) with a verified
    fallback; MJPG for the 1080p path to fix low FPS; stabilized UUID generation (no more UUID churn
    on replug/restart).
  • Performance / stability: faster Windows camera init, FPS-independent pose smoothing, thread-safety
    and resource-leak fixes, jitter/FPS fixes.
  • Tooling: GitHub Actions CI, install scripts (install.sh/install.bat), run scripts,
    configurable keyboard shortcuts, README/CHANGELOG/CONTRIBUTING.

B. Codebase audit (11 tracked items + security) — all complete

Goal: fewer places for bugs to hide, easier to navigate, no regressions.

Bug fixes (behavioral):

Structure (pure code movement, identical external API):

Security / supply-chain:

  • All dependency versions pinned exactly (==) in requirements*.txt.
  • Pose model pinned to a versioned URL and SHA-256-verified on download/cache (rejects a
    tampered or corrupt model).

Quality gates (now blocking in CI):

  • pylint enforced at 10.00/10; tests grew ~51 → 96; pre-commit hook added.
  • Added the first coverage for coordinate transforms, quadrant logic, pose-detect() thread-safety,
    zoom geometry, buffer scrubbing, and a gui import-cycle guard.

Full per-item detail in TODO.md; sharp edges documented in CLAUDE.md.


✅ Verification

  • CI green on the branch head: test + lint both pass (Python 3.10, Ubuntu, with GL libs).
  • Locally: 96 tests pass, pylint 10.00/10, pre-commit clean, import main OK (Python 3.11 venv).
  • Not yet exercised on real camera hardware — that's tonight's customer-machine test.

🔧 Setup on the customer machine (important)

  1. Recreate the venv — dependencies are now pinned, so an old venv may have mismatched versions:
    python3.11 -m venv venv        # use a 3.9–3.12 interpreter (not 3.13+)
    <venv>/pip install -r requirements.txt
    
  2. First pose-enabled run downloads the ~29 MB model (now checksum-verified) to
    ~/.cache/framelab/models/ — needs network access once.

🧪 Real-hardware smoke checklist (the behavior changes most worth confirming)

📝 Known doc follow-up (non-blocking)

CHANGELOG.md predates the audit — it still lists the removed Preferences dialog and the old
dev-v.1.1.0 branch name. Worth a sync pass before final release, but it doesn't affect runtime.

🤖 Generated with Claude Code

cu-andrefecto and others added 30 commits November 16, 2025 10:32
* Fixed issues with recording not showing wireframes
* Trying to fix a bug where cameras get initialized to a low resolution
  even though they're higher
* Trying to fix a bug where camera FPS shows higher than possible
* Moved the UI around a little to make the menu make sense
* Added a bunch of per-camera settings
* Fixed issues with the wireframe settings not taking effect
* Fixed issues where having pose enabled by default didn't actually work
* Added the ability to configure camera settings like resolution, FPS,
  video codecc
## Bug Fixes:

1. **Video Loading** - Fixed TypeError when loading videos
   - Videos with None max_display dimensions now load correctly
   - Added None check before dimension comparison (camera.py:180)

2. **Camera UUID Consistency** - Fixed per-camera settings not applying
   - FPS display now uses camera_uuid instead of camera_id (camera.py:514)
   - Pose estimation settings use camera_uuid (camera.py:874-884)
   - Per-camera body parts, angles, and side view now persist correctly

3. **High-Resolution Camera Support** - Fixed 1080p/4K cameras on Windows
   - Windows default 640x480 resolution now overridden
   - Requests 4K (3840x2160) on init, falls back to camera max
   - 1080p and 4K cameras now use native resolution

4. **Video Memory Optimization** - Fixed potential memory issues
   - 4K video loading now limits display texture to 1920x1080
   - Prevents oversized DearPyGUI textures
   - Improves UI responsiveness for high-res videos

## Documentation:

- Created CLAUDE.md - comprehensive codebase guide for AI assistance
- Added section markers to major files (camera.py, pose/, gui/, utils/)
- Enhanced module docstrings with architecture notes
- Documented commenting conventions for easier code navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
## Performance Enhancements:

1. **60 FPS Camera Support** - Removed artificial FPS throttling
   - Live cameras now run at native hardware FPS
   - Eliminates motion blur from frame rate limiting
   - Significantly improved smoothness for high-FPS cameras
   - Better GPU/CPU utilization (removed sleep delays)

2. **Enhanced Pose Estimation** - Upgraded MediaPipe model
   - Increased model_complexity from 1 to 2 (Heavy model)
   - Better accuracy at cost of more GPU usage
   - Disabled segmentation for better performance
   - GPU automatically optimized by TensorFlow Lite delegate

3. **Pose Smoothing** - Reduced wireframe jitter
   - Added exponential moving average (EMA) smoothing
   - Smoothing factor 0.3 balances responsiveness with stability
   - Increased tracking confidence from 0.5 to 0.7
   - Angle measurements much easier to read

## Bug Fixes:

4. **Pose Estimation Disable Crash** - Fixed MediaPipe shutdown error
   - Fixed "packet timestamp mismatch" race condition
   - Proper shutdown: flag → wait 100ms → release resources
   - Added try-catch wrapper for graceful error handling
   - Clears smoothed landmarks cache on release

5. **Pixel Format Filtering** - Fixed invalid formats in dropdown
   - Improved FOURCC to string conversion for non-printable chars
   - Filters out formats with "?" or "Unknown"
   - Selected formats now persist correctly
   - Prevents restart failures from invalid codes

## UI Improvements:

6. **Adjustable Angle Arc Radius** - Configurable visualization size
   - Added angle_arc_radius setting (default: 20px, down from 30px)
   - New slider in Wireframe → Appearance (range: 10-80px)
   - Smaller arcs reduce clutter, easier to read angles
   - Persists in settings.json

All changes extensively tested with 1080p/4K cameras at 60 FPS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Significantly improved startup time on Windows systems by optimizing
camera detection and initialization:

- Use DirectShow backend (CAP_DSHOW) on Windows for 2-3x faster camera detection
- Early exit after 2 consecutive failed camera indices (instead of checking all 10)
- Reduced max camera indices to check from 10 to 8
- Reduced camera adjustment delays (0.1s → 0.05s)
- Reduced frame read retry attempts (3 → 2) and delays (0.05s → 0.03s)

Expected improvement: 70-80% reduction in startup time (from 15-20s to 3-5s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add recording_lock to prevent race condition between capture thread and stop_recording()
- Release pose estimator resources on exception to prevent MediaPipe leaks
- Release VideoCapture on init failure to prevent handle leaks
- Guard against empty frame buffer in toggle_live_pause() to prevent IndexError
- Wrap frame buffer resize with frame_lock for thread safety
- Add retry limit (300) for consecutive cap.read() failures to prevent infinite loop
- Guard seek_to_frame against total_frames=0
- Use try/finally to ensure video_writer.release() on encoding failure
- Extract _get_quadrant_sizes() helper replacing 4 duplicate calculations in layout.py
- Extract _create_mouse_handlers() and _create_quadrant_content() eliminating ~170 duplicate lines
- Remove dead create_dividers() function
- Fix bare except clauses to use except Exception
- Clean up handler registries on layout rebuild to prevent memory leak
- Restore divider positions from settings on startup
- Fix broken settings tests to use UUID-based API
- Remove unused Path imports, fix misleading comment, simplify redundant conditional

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce CameraSource ABC so FrameLab can support industrial cameras
(Basler GigE/USB3 Vision) alongside standard USB webcams. Detection now
returns descriptors instead of bare ints, and the factory pattern selects
the right backend at runtime. pypylon is an optional dependency — existing
webcam-only setups are unaffected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use time-constant-based EMA instead of fixed alpha so landmark smoothing
is consistent regardless of camera frame rate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix zoom label updates in live camera controls using configure_item
  instead of set_value (buttons ignore set_value)
- Add missing tag to video pause button so slider-drag updates it
- Fix off-by-one in video slider seek (was seeking past last frame)
- Guard texture updates with does_item_exist during layout rebuilds
- Protect frame buffer access with frame_lock to prevent race conditions
- Use list(state.cameras) snapshots to prevent iteration errors
- Replace O(n*m) nested loops with O(1) dict lookups in all handlers
- Clamp live buffer slider values to [0.0, 1.0]
- Fix get_position() denominator so video slider reaches 1.0 at end

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pylint setup:
- Add .pylintrc with sensible defaults for DearPyGUI/OpenCV/MediaPipe
- Fix import ordering (stdlib before third-party) across all modules
- Remove unused imports and variables
- Add explicit encoding to file open() calls
- Remove redundant reimports (time module)
- Score: 9.57/10

AI-friendliness improvements:
- Replace magic tuples with LandmarkAdjustment NamedTuple in pose estimator
- Extract DEFAULT_CAMERA_WIDTH/HEIGHT/FPS constants from magic numbers
- Remove dead code (unused frozen_landmarks attribute)
- Add type hints to public API methods (estimator, renderer, camera, settings)
- Rename update_texture() to process_and_render_frame() (with alias)
- Rename "None" string to "Unassigned" in quadrant combo UI
- Add clarifying comment for Settings getter/setter asymmetry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update menu paths, keyboard shortcuts, settings format, angle list,
Python version, project structure, and UI labels to match current code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lower default camera resolution from 4K to 1080p to prevent macOS
AVFoundation from falling back to 1552x1552 square format. Show
full UI with empty quadrants when no cameras are detected so users
can still load videos. Add auto-dependency-update to launcher scripts
and cache-clearing scripts for troubleshooting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cameras now use their native OS/driver defaults instead of forcing
resolution/FPS/pixel format. This fixes initialization failures on
Windows where the release/reopen cycle and DirectShow format
negotiation caused cameras to fail on restart after settings changes.

UUID fingerprint changed from resolution-dependent (unstable) to
backend+index (stable across restarts). Old assignments are
automatically migrated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DirectShow (CAP_DSHOW) defaults to 640x480 and requires explicit
resolution requests. Media Foundation (CAP_MSMF) auto-negotiates the
camera's best native resolution, matching AVFoundation behavior on
macOS. Added a fallback for legacy backends that still default low.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fication

On Windows, try Media Foundation first (auto-negotiates best resolution),
fall back to DirectShow if unavailable. Store which backend worked in
the camera descriptor so the same backend is used when creating the
camera source. When the 1080p fallback is triggered, verify the stream
actually works by reading a test frame — revert if it breaks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Uncompressed YUY2 at 1080p saturates USB bandwidth, capping cameras
like the Razer Kiyo Pro at ~18 FPS. Setting MJPG (compressed) format
before the resolution request allows full frame rate (30-60 FPS).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make Basler cameras the primary detection target with OpenCV webcams as
fallback. Add native GenICam controls (exposure, gain, auto modes, FPS),
USB bandwidth limiting for multi-camera setups, Basler-specific settings
persistence, and a dedicated Camera Controls dialog in the UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix workflow push trigger (dev-v.1.1.0 -> dev-v1.1.0) so the dev branch
  actually runs CI; rename workflow to "CI"
- Add a blocking pylint job (parallel to tests); both jobs cache pip
- Add requirements-dev.txt (pinned pylint + pre-commit, over requirements.txt)
- Add .pre-commit-config.yaml with a local pylint hook
- Bring codebase to pylint 10.00/10: commented .pylintrc disables for
  framework conventions (DPG callback args, too-many-* family, state.py
  module globals) plus behavior-preserving code fixes

Bonus fixes surfaced by pylint:
- Remove ~225 lines of dead nested functions in create_menu_bar
- Fix cell-var-from-loop bug: per-camera Settings/Proc-Amp dialogs showed the
  last camera's name in their titles
- Rename Camera._apply_persisted_settings -> apply_persisted_settings
- Fix pre-existing failing test (test_pose_renderer::test_initialization
  asserted attributes PoseRenderer never defined)
- Update CLAUDE.md (CI/CD + setup) and add TODO.md tracking doc

Verified locally (py3.11): pylint 10.00/10, 51 tests pass, pre-commit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first real CI run (after the branch-typo fix) exposed a pre-existing gap:
all 8 test_pose_estimator tests error with
  OSError: libGLESv2.so.2: cannot open shared object file
because PoseEstimator() loads MediaPipe, which links libGLESv2 even on the CPU
delegate, and the bare ubuntu runner lacks it.

- Install libgl1/libegl1/libgles2 in the test job before pip install
- Bump actions/checkout@v3->v4 and setup-python@v4->v5 (Node 20 deprecation)
- Correct CLAUDE.md: pose tests need GL libs (not fully display-free)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One landmarker per camera had detect() + smoothing/adjustment state reachable
from the capture thread (recording branch) and the main render thread with no
synchronization; concurrent MediaPipe detect() during recording-with-pose is the
intermittent-crash source.

- Add a threading.Lock + _closed flag to PoseEstimator guarding process_frame,
  get_landmarks (now returns None for None/closed results), the four manual-
  adjustment setters, and release() (idempotent; waits for in-flight detect())
- Drop the time.sleep(0.1) hack in Camera.disable_pose_estimation (release() is
  now lock-safe)
- Add concurrency + use-after-release regression tests

No change to detection output, coordinates, smoothing, or recording semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rks)

Manual pose adjustment didn't line up with visible joints when zoomed. The render
loop detects on the zoomed-then-resized frame (landmarks in displayed space), but
the editor re-detected on the raw, un-zoomed frame for hit-testing, so clicks
tested against the wrong positions at zoom > 1.

- Camera publishes current_landmarks (the dict it just drew) and exposes one
  transform, display_to_frame(), used for all mouse<->landmark conversions
- PoseEditor.get_landmark_at_position reuses current_landmarks + display_to_frame
  instead of re-detecting (fixes alignment AND removes the redundant detect() -
  resolves TODO #4); update_drag uses the same transform
- layout.py mouse handler updated to the new update_drag signature
- Tests: pure display_to_frame mapping + new test_pose_editor.py hit-test

No behavior change at zoom = 1; one fewer detect() call site (complements the
thread-safety fix). Detection still runs on the cropped frame (out of scope).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Delete the no-op "Pose Estimation Quality" menu and the pose_max_width
  setting (PoseEstimator never downsampled); drop the dead camera.pose_max_width
  writes and the test assertion. Pose still runs at native resolution.
- Delete gui/preferences_dialog.py (121 lines, never imported/instantiated)
- Remove the unused update_texture alias and fix stale docstring/comment
  references to update_texture / _process_frame_for_display
- Prune CLAUDE.md's now-empty "Dead / no-op features" section

Pure removal, no behavior change. pylint 10.00/10, 57 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#5 1/3)

Move whole functions out of the 2200-line layout.py:
- gui/controls.py: create_video_controls, create_live_camera_controls
- gui/divider.py: divider drag (hit-test, ghost line, mouse down/move/release)
- gui/input_handlers.py: keyboard shortcuts, frame stepping,
  register_global_mouse_handlers, _create_mouse_handlers

Cross-gui calls use function-local imports (existing codebase pattern) to avoid
cycles. main.py imports register_global_mouse_handlers + step_frame_* from
gui.input_handlers. layout.py: 2200 -> 1235 lines. Pure movement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
andrefecto and others added 30 commits May 26, 2026 10:10
These handlers logged many INFO lines per mouse click / keypress / joint
drag, flooding logs at the default INFO level and burying real signal.
Demoted the per-event spam to DEBUG:
- gui/divider.py on_mouse_click_handler: the whole per-click block incl.
  the two 60-char "=" banners (the error log stays).
- gui/input_handlers.py: KEY PRESS/RELEASE, Keyboard zoom, spacebar/number
  -key targeting, pause/play toggle, and the two image CLICK lines.
- pose/pose_editor.py: Started/Converted/Finished dragging landmark.
- gui/controls.py on_speed_change: callback + parsed-speed diagnostics.

Kept at INFO the genuine state-change events: recording toggle + saved,
screenshot toggle + saved, "Dividers repositioned and saved", and
"Cleared all pose adjustments". Logging level only — no logic changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gui.quadrants had real, bug-prone logic with no tests. Added
TestQuadrantManipulation: remove_quadrant's position-shift (a camera above
the removed quadrant shifts down; the dual conditions pos>q and p<q must
stay consistent), the inactive-quadrant no-op guard, move_camera_to_position
swap + the KeyError on an absent key, toggle enable-sorts / disable-removes,
and add_camera_to_quadrant eject + the "(None)"/"Load Video..." branches.

Drives the functions over the state globals with gui.layout.rebuild_camera_layout
and gui.quadrants.save_camera_positions patched out; snapshots/restores the
three globals (active_quadrants is reassigned, so restore by reassignment)
and sets them explicitly per test for order-independence. 13 tests in the
file; full suite still green (no state leak).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The zoom/pan crop math was inline in process_and_render_frame and
unobservable (output is always resized back to w x h), so its integer
truncation and edge-clamping were untested. Extracted a pure static
Camera._zoom_crop_box(w, h, zoom_level, cx, cy) -> (x1,y1,x2,y2) with the
math moved verbatim (incl. the int()/`// 2` and the right/bottom
re-adjustment); it returns the full frame for zoom <= 1.0 and the caller
keeps its `if zoom_level > 1.0` guard. No behavior change.

Added TestZoomCropBox: centered 2x box, left/right edge clamps (window
shifted back so width is preserved), the no-zoom full-frame branch, and a
non-divisible zoom (640/3 -> 213) that locks the truncation against a
future "obvious cleanup".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Buffer scrubbing (camera/camera.py): step_buffer_forward/backward clamp at
the last/first frame; seek_buffer_position maps 0..1 -> int(pct*(len-1));
all three no-op (return False, no mutation) when not paused or the buffer
is empty (the empty-buffer guard prevents a negative index).

Angles (pose/renderer.py): calculate_all_angles omits a joint with an
incomplete landmark set (asserted absent, not zeroed) and rounds present
ones to 1 decimal; calculate_angle on a zero vector must not return NaN
(guards the +1e-6 / np.clip).

Refreshed CLAUDE.md's test-coverage note (now 96 tests; lists the added
coverage) and marked TODO #10 done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First step of splitting the 1146-line Camera god-class via mixins (pure
code movement, identical external API + threading). Moved the zoom/pan
state actions and view-geometry transforms — _zoom_crop_box (static),
zoom_in/out, reset_zoom, display_to_frame — to camera/_zoom.py; Camera now
inherits ZoomMixin. Cross-mixin calls (process_and_render_frame's
self._zoom_crop_box) resolve via MRO on the shared self.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved video playback (play/pause/loop/speed/seek) and live frame-buffer
scrubbing (toggle_live_pause/step_buffer_*/seek_buffer_position/get_position)
to camera/_playback.py; Camera inherits PlaybackMixin. Pure movement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 3/6)

Moved start_recording/stop_recording (MP4 encode + angles JSON export) and
take_screenshot to camera/_recording.py; Camera inherits RecordingMixin.
The recording_lock drain in stop_recording and the capture-thread append
are unchanged (same instance lock/state). Dropped the now-unused os/datetime
imports from camera.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved enable_pose_estimation/disable_pose_estimation (and the
'from pose import PoseEstimator, PoseRenderer' dependency) to
camera/_pose.py; Camera inherits PoseMixin. Detection still runs in the
render/capture paths via the shared pose_* state. Same camera->pose import
edge, just relocated — no new cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved create_display_texture and process_and_render_frame (the main-thread
render: frame_lock read, zoom crop via self._zoom_crop_box, pose overlay,
FPS HUD, texture push) to camera/_render.py; Camera inherits RenderMixin.
Dropped the now-unused dpg/numpy imports from camera.py (cv2 stays for
initialize/_capture_loop).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved the background capture thread (_capture_loop + start_capture) to
camera/_capture.py; Camera inherits CaptureMixin. camera.py is now 415
lines (from 1146) — the spine: __init__, initialize/settings, position_key,
naming, release. Dropped the now-unused `import time` from camera.py.

Threading is unchanged: _capture_loop (bg thread) and process_and_render_frame
(main thread) still share self.frame_lock/self.recording_lock and the same
instance state — mixins only relocate the method source.

Also: dropped the resolved C0302 (too-many-lines) .pylintrc disable — both
tracked splits (#5 layout, #11 camera) are done and the largest file is now
519 lines. Added a "Map of the camera/ package" to CLAUDE.md and marked
TODO #11 done. pylint 10.00/10, 96 tests pass, pre-commit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update the 1.1.0 entry to match what actually ships: add 'Codebase audit
& hardening' (thread-safe pose, zoom-aligned editing, native-res video
texture, stable quadrant keying) and 'Security' (pinned deps + SHA-256
model verification) sections; refresh the test/CI bullets (96 tests, the
blocking lint job). Remove now-false references to the deleted Preferences
dialog and the removed Pose Estimation Quality feature, fix the FPS-toggle
location, correct the dev-v1.1.0 branch typo, the date, and the 3.9-3.12
Python range.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation for the in-app updater. New version.py is the single source of
truth (__version__ = "1.1.0"); main.py logs it at startup and shows it in
the viewport title. Installers now bump the Python floor 3.8 -> 3.9
(MediaPipe requirement), source the macOS .app CFBundleVersion / Linux
.desktop Version from version.py instead of hardcoding 1.1.0, and warn (not
fail) when git or a git checkout is missing (auto-update needs it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ate 2/7)

Settings gains auto_check_updates (default True), skipped_update_version,
and last_update_check (throttle), wired through load/save/setters; old
settings files load the defaults (migration). Tests cover defaults,
round-trip, and migration from a pre-update file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ate 3/7)

parse_version/is_newer (numeric MAJOR.MINOR.PATCH, gets 1.10.0 > 1.9.0
right), fetch_latest_release (public GitHub Releases API over certifi-backed
HTTPS via an injectable opener; all failures swallowed so startup never
blocks; tag validated against ^v?\d+\.\d+\.\d+ before it could ever reach
git), should_notify (auto-check + newer + not-skipped), and is_check_due
(6h throttle for the unauth API). 17 unit tests, no network. UI/process
wiring comes in later phases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
update_capability(repo_dir) probes (via an injectable subprocess runner)
whether the updater can drive this install: git on PATH, a git work-tree
with an origin remote, and a clean *tracked* tree (untracked/ignored files
like settings.json/venv are fine; detached HEAD is allowed since the updater
pins release tags). Returns (can_apply, reason) so the UI can offer "Update
now" or fall back to manual instructions instead of failing. 5 mocked-git
tests for each degrade path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ers (auto-update 5/7)

Add update.sh / update.bat (the self-updater the app invokes on "Update
now"): wait for the app PID to exit (settle + poll, ~30s ceiling), git fetch
+ checkout the released tag, pip install into the repo's venv, relaunch; on
failure stay on the old ref and relaunch the old version. Windows runs in a
visible console so git/pip progress shows.

Critical fix: install.sh/.bat no longer *generate* run_framelab.* — those are
tracked files, and the updater's `git checkout <tag>` aborts on any local
modification, so a regenerated-vs-committed drift would break every update
(the .bat even differed: %~dp0 vs %SCRIPT_DIR%). Installers now just chmod the
committed launcher + update.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…o-update 6/7)

New gui/updates.py: a background daemon thread runs utils.updater.fetch
(never blocks startup / breaks offline) and publishes the result into
gui.state; main.py's render loop calls poll_and_prompt() once per frame to
show — on the main thread, per DPG's UI-thread rule — a modal: "FrameLab vX
is available" with Release notes / Update now / Later / Skip this version.
If update_capability() says auto-update isn't possible (no git / dirty tree),
the dialog shows manual `git pull` instructions instead of "Update now".

"Update now" warns if recording, then spawns the committed update.sh/.bat
detached (POSIX start_new_session; Windows CREATE_NEW_CONSOLE) with repo dir
+ tag + PID and calls dpg.stop_dearpygui() so the updater can take over.
main.py kicks the auto-check after startup; File > Check for Updates runs it
manually (force_show, ignoring throttle/skip, with "you're up to date"
feedback). gui.updates added to the import-cycle guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CONTRIBUTING.md gains a "Releasing FrameLab (maintainers)" section: bump
version.py, merge to main, and publish a GitHub Release with a matching
v<version> tag (a push to main alone won't trigger an update). Documents the
client update flow, the detached-HEAD state on updated installs, the
committed-launcher invariant, and the git-clone prerequisite. CLAUDE.md gains
a short auto-update pointer (updater module split + the don't-hand-edit-the-
launchers sharp edge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expand Basler support with white balance, gamma, black level, ROI/binning,
and pixel-format controls (BaslerCameraSource + a tabbed controls dialog,
gating absent nodes via has_node), plus full-config backup/restore to a .pfs
file via pylon.FeaturePersistence (same format as the pylon Viewer).

New per-control values persist in the 'basler' settings dict and are re-applied
on startup in dependency order (binning -> ROI -> pixel format -> auto modes ->
manual values -> gamma/black level -> fps); a .pfs restore applies live then
reads the controls back so the import sticks through the normal replay path.
ROI/binning resolution changes route through Camera.refresh_dimensions(), which
recreates the display texture and rebuilds the layout to keep the texture-size
invariant. Adds source-layer unit tests for all new methods.

Also bundles pending FPS-overlay capture/render tweaks from a prior session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bring the README current with dev-v1.1.0: rewrite the installation/launch
flow (committed run_framelab/update launchers, git check), add an "Updating
FrameLab" section for the in-app auto-updater, expand the Basler/industrial
camera coverage (tabbed GenICam controls + .pfs backup/restore), and refresh
the now-stale Project Structure tree, dependencies, and contributing pointers.

Add a tuned .markdownlint.json (disables line-length / inline-HTML / first-line
heading / ordered-list-prefix rules that conflict with the repo's style;
duplicate-heading siblings-only) so markdownlint runs clean (0 errors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nnotations

Add a DartFish-style manual measurement mode as an alternative to automatic
pose detection: the user places N independent, labeled 3-point angle
measurements (arm-vertex-arm) and the angle at the vertex is shown live on the
frame. This mode never runs MediaPipe, so it sidesteps unreliable auto-detection
(bad angle/lighting) and the re-detection-overwrites-offsets problem.

- pose/manual_angles.py: pure, headless-testable model (AngleMeasurement +
  ManualAngleTool) keyed by frame number; seed-and-drag handles; JSON round-trip.
- camera/_manual_angles.py mixin + Camera wiring: enable/disable/add and, for
  video files, persist measurements to a {video}_manual_angles.json sidecar
  (reload + re-edit on reopen). Live cameras are in-session only.
- Points stored in native pixels (zoom-independent) via new ZoomMixin
  native_to_view/view_to_native; a dedicated displayed_frame index gives exact
  frame keying. Overlay drawn after the pose block; mouse drag routed in
  _create_mouse_handlers (edit only on a paused/seeked frame).
- Per-quadrant "Angles" button (video + live) opens a management dialog
  (enable, add, relabel, live readout, delete, clear).

164 tests pass (17 new); pylint 10.00/10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Relocate TODO.md under docs/ and add docs/IDEAS.md capturing roadmap ideas
(camera-control quadrant, bike-measurement export, quadrant pop-out, drag/aero
calculations, athlete-history quadrant, navigation simplification).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scan with vulture (min-confidence 60) over the whole codebase; every
candidate cross-checked with repo-wide grep (incl. tests) before deletion.

- utils/settings.py: drop 9 zero-caller setters/getters (dialogs persist
  via direct attribute assignment).
- camera/detection.py: delete the dead hardware-info subsystem
  (per-platform id helpers, _hardware_info_cache, get_camera_hardware_info,
  fourcc<->string converters) + now-unused platform/re/subprocess imports
  (417 -> 208 lines).
- pose/estimator.py: drop per-landmark clear_manual_adjustment (the used
  method is clear_all_adjustments); utils/logger.py: drop log_exception;
  gui/divider.py: drop unused DIVIDER_* constants; gui/state.py: drop
  slider_active; camera/source.py: drop unread self._camera_id.
- Remove the "Reset All to Defaults" camera feature entirely (dead AND
  silently broken: wrote phantom legacy attrs, never cleared the real
  camera_settings_by_uuid) — function, menu item, and wiring.
- Tests: drop cases that only exercised removed test-only methods.
- Docs: clean TODO.md, mark item #1 done; trim IDEAS.md; update CLAUDE.md.

Verified: pylint 10.00/10, 162 tests pass, pre-commit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FrameLab did no font setup, so DearPyGui fell back to its built-in bitmap
font (ProggyClean) at ~13px — blocky and hard to read. Load a bundled
anti-aliased TrueType font (Roboto) oversampled 2x and bind it globally.

- gui/fonts.py: setup_fonts() (fail-safe — logs + falls back to the bitmap
  font on any error, never blocks startup) + DPG-free _resolve_font_path().
- main.py: call after dpg.setup_dearpygui(), before show_viewport().
- assets/fonts/: bundle Roboto-Regular/Bold.ttf + Apache-2.0 LICENSE.
- tests/test_fonts.py + gui.fonts in the import-smoke list.

First (font-legibility) slice of the "Easier navigation" idea (docs/IDEAS.md);
tracked as item #2 in docs/TODO.md. pylint 10.00/10, 166 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a discoverable top-level "Quick Settings" menu (2nd, after File) with
one-click checkboxes that apply to ALL cameras at once, removing the
per-camera-dialog + Save-per-camera friction.

- gui/dialogs/quick_settings.py: DPG-free set_pose_estimation_all() /
  set_fps_overlay_all() helpers (loop state.cameras, persist via settings) +
  thin on_pose_toggle_all / on_fps_toggle_all callbacks. The FPS handler sets
  the global default AND per-UUID overrides for live cameras so the toggle wins
  uniformly; video files follow the global default.
- Centralized the pose path: removed wireframe.on_pose_toggle (moved into
  set_pose_estimation_all), repointed the Wireframe "Enable Pose Estimation"
  item to the shared callback (tag wf_pose_toggle); the callback syncs both the
  Wireframe and Quick Settings checkmarks so they never desync. Updated the
  gui/dialogs/__init__.py re-exports.
- tests/test_quick_settings.py (pose enable/disable-all + FPS global/per-camera
  persistence); gui.dialogs.quick_settings added to the import-smoke list.

Second slice of the "Easier navigation" idea (docs/IDEAS.md); tracked as item
#3 in docs/TODO.md. pylint 10.00/10, 170 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reorganize the menu tree to reduce scanning/click-depth (7 -> 6 top-level
menus). Pure reorder/relabel within create_menu_bar() — no behavior or
callback changes.

- New order: File | Quick Settings | Cameras | Pose | Quadrants | Help.
- Remove the dead "View -> Stats" placeholder (only logged "not implemented").
- Fold the lone "Playback" menu into Cameras as "Playback Settings...".
- Rename "Camera Settings" -> "Cameras" and "Wireframe" -> "Pose".
- Move "Keyboard Shortcuts..." + "Check for Updates..." into a new Help menu.
- Keep all item tags (main_menu_bar, qs_*, wf_pose_toggle, side_view_*) so the
  layout-rebuild and checkmark-sync paths keep working.

Third slice of the "Easier navigation" idea (docs/IDEAS.md); tracked as item
#4 in docs/TODO.md. pylint 10.00/10, 170 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add "Camera Controls" as a selectable quadrant content type so common
per-camera actions aren't buried in menus. Picking it from a quadrant's
dropdown replaces the feed with a compact panel that targets one live camera.

Panel (launchers + quick toggles, no inline sliders): camera picker, Pose
Estimation + Show FPS checkboxes, zoom -/+/Reset, and launcher buttons for the
existing General Settings / Configure Joints / Camera Controls (Basler) or
Video Proc Amp (webcam) dialogs. Reuses Camera methods + dialog entry points.

- gui/control_panel.py: create_control_panel(quad_position).
- gui/state.py: control_quadrants (set) + control_panel_camera_keys (session).
- utils/settings.py: persisted global control_quadrants (init/load/save).
- gui/quadrants.py: CONTROL_PANEL_OPTION + save_control_quadrants(); add/eject/
  clear so a control panel and a camera are mutually exclusive per quadrant.
- gui/layout.py: render the panel for control quadrants + dropdown option.
- main.py: _restore_control_quadrants() at startup (control wins over a camera
  at the same slot), both no-camera and normal paths.
- tests/test_control_panel.py (assignment logic + persistence round-trip);
  gui.control_panel added to the import-smoke list.

Opt-in/persisted (no auto-placement on fresh installs). Promoted from
docs/IDEAS.md; tracked as item #5 in docs/TODO.md. pylint 10.00/10, 176 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the loose setup + maintenance scripts out of the repo root:
- install.sh / install.bat   -> install/
- clear_cache.sh / .bat       -> utilities/

Kept at root (load-bearing for auto-update): run_framelab.* and update.* —
gui/updates.py spawns update.* from _REPO_ROOT and update.sh execs
./run_framelab.sh, so moving them would break the in-app updater.

Each moved script now operates on the repo root (its parent): install.sh
derives REPO_DIR=$SCRIPT_DIR/.. (cd, git -C, and the generated .app/.desktop
launcher paths); install.bat derives REPO_DIR via pushd "%~dp0.." (cd + the
shortcut target/working dir); both clear_cache.* cd to "..".

Docs: README install commands + permission tip + Project Structure tree;
CLAUDE.md note on the new layout and why the launchers stay at root.

Scoped out moving the Python into a subfolder (would break imports/CI/installers).
Promoted from docs/IDEAS.md; tracked as item #6 in docs/TODO.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add "Athlete History" as a selectable quadrant content type that browses the
current athlete's recordings on disk and opens one with a click. Picked after
the "Quadrants pop out" idea was dropped (DearPyGui is single-viewport — no
real second OS window for another monitor; recorded in docs/IDEAS.md).

Phase 1:
- athlete/manager.py: list_recordings(athlete, bike) -> bike folder's *.mp4 as
  full paths, newest-first (+ convenience export). Filesystem-only.
- gui/history_panel.py: create_history_panel() lists bikes (current expanded)
  -> recordings as buttons; clicking opens the video in a free feed quadrant
  via load_video_file (_target_quadrant_for_open). Refresh re-scans.
- state.history_quadrants + persisted settings.history_quadrants; gui/quadrants
  gains HISTORY_PANEL_OPTION, save_history_quadrants, and a shared
  _clear_panel_modes() so control panels, history panels, and cameras are
  mutually exclusive per quadrant. gui/layout renders it + adds the dropdown
  option; main._restore_panel_quadrants restores both panel sets at startup.
- tests/test_athlete_history.py (list_recordings + history-quadrant logic +
  persistence round-trip); gui.history_panel added to the import-smoke list.

Remaining phases: thumbnails, screenshot/angle-JSON browsing, richer viewer.
Tracked as item #7 in docs/TODO.md. pylint 10.00/10, 183 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a toggleable, zero-cost-when-off perf layer (utils/perf.py) gated by the
FRAMELAB_PERF env var to diagnose Basler choppiness / wrong on-screen FPS:

- capture thread: rolling delivery FPS + cap.read grab latency, and per-30-frame
  Basler stream-grabber buffer/drop stats with deltas
- render: per-stage timing (copy/zoom/pose/cvtColor/normalize/set_value/whole),
  render-call FPS, and a log of which source the HUD FPS reflects
- main loop: render_dearpygui_frame vs full-iteration time + loop FPS (exposes
  the DPG vsync ceiling)
- basler_source: defensive get_grab_statistics() probing transport-specific nodes
- main.py: quiet MediaPipe glog/clearcut telemetry so perf lines stay readable

Add run_framelab_perf.sh/.bat launchers that set FRAMELAB_PERF=1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants